Goroutines and Channels
CIS 193 – Go Programming
Prakhar Bhandari, Adel Qalieh
CIS 193
Prakhar Bhandari, Adel Qalieh
CIS 193
In the previous class, we learned about concurrency and parallelism
Now, it's time to see how to apply these concepts to Go
Goroutines
goroutine
is a lightweight, concurrently executing activityf() // call f() and wait for it to return go f() // create a new goroutine that calls f(), don't wait for it to return
Note: When the main function returns, all goroutines are abruptly terminated and the program exits
Each OS thread has a fixed-size block of memory for the stack, which can be up to 2 MB
Problematic for Go programs, which are highly concurrent and can have hundreds of thousands of goroutines
Goroutines start with a small stack space (usually 2 KB) and can grow / shrink as needed
Takeaway: Goroutines are much cheaper than threads
We saw in our previous lecture that there are cases when concurrent programs need to be able to communicate
A channel is a communication mechanism that lets one goroutine send values to another goroutine
Each channels has a particular type of data
Syntax
ch := make(chan int) // channels must be created before they are used with make ch := make(chan int, 4) // 4 is the buffer length
Channels are reference types (like maps) and can be compared with == if they are of the same type - this checks if they refer to the same channel
Channels have two main operations, send and receive
Sending transmits a value from one goroutine, through the channel, to another goroutine executing a corresponding receive operation
Send statements
ch := make(chan int) ch <- 34 // sending 34 to the channel
Receive statements
ch := make(chan int) <-ch // receive with discarded result x := <- ch // receive with saved result
Creating a channel with make(chan T)
will make an unbuffered channel
This is equivalent to initializing with make(chan T, 0)
For an unbuffered channel, a send operation will block the sending goroutine until another goroutine executes a corresponding receive operation on the same channel
Since sends and receives here wait until the other side is ready, we can use this to synchronize goroutines
Syntax
ch := make(chan T, i) // T = type, i = integer corresponding to buffer length
Send operation inserts element at back of queue, receive operation removes element from front
Can use len()
and cap()
to see the number of elements currently in the channel and total capacity of the channel
What gets printed?
hello := make(chan string, 3) hello <- "1st" hello <- "2nd" hello <- "3rd" fmt.Println(<-hello) fmt.Println(<-hello) fmt.Println(<-hello)
Assume we have a request() function that gets data from a url
func fastestQuery() string { responses := make(chan string, 3) go func() { responses <- request("asia.server.com") }() go func() { responses <- request("europe.server.com") }() go func() { responses <- request("americas.server.com") }() return <-responses // return the quickest response }
Closing a channel indicates that no more values will be sent
Syntax
close(ch)
Subsequent sends will cause a panic
Subsequent receives will yield the values that have been sent, once they run out, receives will yield the zero value of the channel type
How to check if the channel is actually closed?
v, ok := <-ch // ok is false if the channel is closed
Looping over channels
for i := range ch {...} // receives values from the channel ch until it is closed
When channels are passed into a function as an argument, they are usually either used to send or to receive
So, we can specify two different unidirectional channel types
Send-only channel
chan<- T
Receive-only channel
<-chan T
func counter(out chan<- int) { for x := 0; x < 10; x++ { out <- x } close(out) } func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out) } func printer(in <-chan int) { for v := range in { fmt.Println(v) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares) }
The select
statement lets a goroutine wait on multiple communications
select
will block until one of the cases can run, then it runs it
If there are multiple cases ready to run, one is chosen at random
The default
case is run if no other case is ready
Syntax
// can be put in an infinite loop for { select { case <-ch1: // ... case x := <-ch2: // ...use x... case ch3 <- y: // ... default: } }
func Deposit(amount int) { mu.Lock() balance = balance + amount mu.Unlock() } func Balance() int { mu.Lock() b := balance mu.Unlock() return b }
var deposits = make(chan int) // send amount to deposit var balances = make(chan int) // receive balance func Deposit(amount int) { deposits <- amount } func Balance() int { return <-balances } func teller() { var balance int // balance is confined to teller goroutine for { select { case amount := <-deposits: balance += amount case balances <- balance: } } } func main() { go teller() }